/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Windows.Forms;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Text.RegularExpressions;
using DaveSexton.DocProject.Engine;
using System.Xml.XPath;
using DaveSexton.DocProject.Sandcastle.Resources;
using DaveSexton.Controls;
using DaveSexton.DocProject.Sandcastle.ContentManagement;
using System.ComponentModel;

namespace DaveSexton.DocProject.Sandcastle.TopicManagement
{
	[Flags]
	public enum TopicManagerChanges
	{
		None = 0,
		Filters = 1,
		DynamicFilters = 2,
		Toc = 4,
		Documentation = 8, 
		Properties = 16
	}

	/// <remarks>
	/// This class loads reference topics asynchronously yet it's thread-safe as long as all access to public members is performed on the main UI thread.
	/// </remarks>
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
	public sealed class TopicManager : MarshalByRefObject
	{
		#region Public Properties
		public const string SandcastleRootReferenceProjectId = "R:Project";

		public MRefBuilderConfigDocument MRefBuilderConfigDocument
		{
			get
			{
				return configDocument;
			}
		}

		public MRefBuilderConfigApiFilter ApiFilter
		{
			get
			{
				return apiFilter;
			}
		}

		public bool ReferenceTopicsLoaded
		{
			get
			{
				return referenceTopicsLoaded;
			}
		}

		public bool ConceptualTopicsLoaded
		{
			get
			{
				return conceptualTopicsLoaded;
			}
		}

		public ConceptualTopicFileManager ConceptualManager
		{
			get
			{
				return conceptualManager;
			}
		}

		public string ConceptualTopicsPath
		{
			get
			{
				return conceptualManager.TopicsPath;
			}
		}

		public string BuildOutput
		{
			get
			{
				return trace.Output;
			}
		}

		public ICollection<CapturedBuildEvent> BuildEvents
		{
			get
			{
				return trace.Events;
			}
		}

		public TopicFilterCollection DynamicFilters
		{
			[System.Diagnostics.DebuggerHidden]
			get
			{
				if (dynamicFilters == null)
				{
					try
					{
						dynamicFilters = TopicFilter.LoadFilters(dynamicFiltersFile);
					}
					catch (System.IO.DirectoryNotFoundException) { }
					catch (System.IO.FileNotFoundException) { }

					if (dynamicFilters == null)
						dynamicFilters = new TopicFilterCollection();

					dynamicFilters.ListChanged += dynamicFilters_ListChanged;
				}

				return dynamicFilters;
			}
		}

		public IXPathNavigable DataSource
		{
			get
			{
				return conceptualManager.Topics.Root;
			}
		}

		public IXPathNavigable TocDataSource
		{
			get
			{
				if (tocNavigator == null)
					return null;

				XPathNavigator root = tocNavigator.SelectSingleNode("/topics");

				if (root != null && root.MoveToFirstChild())
				{
					if (!SandcastleRootReferenceProjectId.Equals(root.GetAttribute("id", ""), StringComparison.Ordinal))
						// Use the root /topics element as the root TOC node since the document does not have an R:Project element
						root.MoveToParent();
				}

				return root;
			}
		}

		public XmlTreeViewDataManager DataManager
		{
			get
			{
				return dataManager;
			}
		}

		public ContentManagement.XmlDocumentationDisplayManager DisplayManager
		{
			get
			{
				return displayManager;
			}
		}

		public Importer Importer
		{
			get
			{
				return importer;
			}
		}

		public int TopicsCount
		{
			get
			{
				return topics.Count;
			}
		}

		public IDocProject Project
		{
			get
			{
				return options.Project;
			}
		}

		public TopicManagerChanges Changes
		{
			get
			{
				TopicManagerChanges changes = TopicManagerChanges.None;

				if (filterDirty)
					changes |= TopicManagerChanges.Filters;

				if (dynamicFilters != null && dynamicFilters.HasChanges)
					changes |= TopicManagerChanges.DynamicFilters;

				if (topicsDirty && conceptualManager.Topics.HasConceptualTopics())
					changes |= TopicManagerChanges.Toc;

				if (contentManager.IsDirty)
					changes |= TopicManagerChanges.Documentation;

				if (propertiesManager.IsDirty)
					changes |= TopicManagerChanges.Properties;

				return changes;
			}
		}
		#endregion

		#region Private / Protected
		private delegate void LoadReferenceTopicsInvoker(bool forceRebuild);

		private const string privateVisibility = "private", familyVisibility = "family";
		private const string assemblyVisibility = "assembly", familyOrAssemblyVisibility = "family or assembly";

		private TopicFilterCollection dynamicFilters, originalDynamicFilters;
		private Topic apiPlaceholderTopic;
		private volatile MRefBuilderConfigApiFilter apiFilter;
		private volatile bool referenceTopicsLoaded, rebuildRequired, conceptualTopicsLoaded, topicsDirty, filterDirty;
		private volatile BuildContext referenceTopicsBuildContext;
		private IAsyncResult buildReferenceTopicsAsyncResult;
		private readonly object syncBuildReferenceTopics = new object();
		private volatile XPathNavigator tocNavigator;
		private volatile ReflectionDocument reflection;
		private LoadReferenceTopicsInvoker referenceBuildContinuation;
		private readonly MRefBuilderConfigDocument configDocument = new MRefBuilderConfigDocument();
		private readonly SandcastleBuildEngine engine;
		private readonly CapturedBuildTrace trace = new CapturedBuildTrace();
		private readonly SandcastleSettings settings;
		private readonly SandcastleProjectOptions options;
		private readonly Importer importer = new Importer();
		private readonly TopicXmlTreeViewDataManager dataManager;
		private readonly string projectName, tocFile, reflectionFile, mRefBuilderConfigFile;
		private readonly string mediaDirectoryRelative, mediaDirectoryProjectRelative, dynamicFiltersFile;
		private readonly XmlDocumentationDisplayManager displayManager;
		private readonly ConceptualTopicFileManager conceptualManager;
		private readonly TopicContentManager contentManager;
		private readonly TopicPropertiesManager propertiesManager;
		private readonly Dictionary<IXPathNavigable, Topic> topics = new Dictionary<IXPathNavigable, Topic>(1024, XmlTopicComparer.Default);
		private readonly Dictionary<IXPathNavigable, TopicData> cachedTopicData = new Dictionary<IXPathNavigable, TopicData>(1024, XmlTopicComparer.Default);
		private readonly Dictionary<IXPathNavigable, TopicData> boundTopicData = new Dictionary<IXPathNavigable, TopicData>(1024, XmlTopicComparer.Default);
		private bool bufferEnabled, fillBufferDuringBind, conceptualTopicsBuffered, referenceTopicsBuffered;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="TopicManager" /> class.
		/// </summary>
		public TopicManager(SandcastleProjectOptions options)
		{
			if (options == null)
				throw new ArgumentNullException("options");

			this.options = options;

			DocProjectEnvironment.ProjectBuildComplete += new EventHandler<DocProjectEventArgs>(DocProjectEnvironment_ProjectBuildComplete);

			engine = (SandcastleBuildEngine) BuildController.GetEngineForProject(options.Project);
			settings = engine.Settings;

			Configuration.Presentation presentation = settings.Presentation;

			tocFile = settings.TocFile;
			reflectionFile = settings.ReflectionXmlFile;
			mRefBuilderConfigFile = System.IO.Path.Combine(settings.ConfigurationPath, options.MRefBuilderConfigurationFileName);
			dynamicFiltersFile = settings.DynamicFiltersFile;
			projectName = options.Project.Name;

			string cssDirectory = System.IO.Path.Combine(settings.TargetDirectory, presentation.CascadingStyleSheetsDirectory);

			displayManager = new ContentManagement.XmlDocumentationDisplayManager(cssDirectory);

			conceptualManager = new ConceptualTopicFileManager(options.Project, settings.TopicsDirectory, settings.TopicsFile);

			dataManager = new TopicXmlTreeViewDataManager(conceptualManager, this);
			dataManager.ElementChanged += new EventHandler<ElementChangedEventArgs>(dataManager_ElementChanged);

			mediaDirectoryRelative = presentation.MediaDirectory;
			mediaDirectoryProjectRelative = System.IO.Path.Combine(settings.RelativeTargetDirectory, mediaDirectoryRelative);

			// Rebase example:  ../Art/image.jpg
			// The rebase path is relative from the Html and Html2 directories
			importer.RebaseDirectory = ".." + PathFormat.MakeVirtual(mediaDirectoryRelative);
			importer.TargetDirectory = System.IO.Path.Combine(settings.ProjectDirectory, mediaDirectoryProjectRelative);
			importer.Rebase = ImporterRebaseAction.RebaseDirectory;

			contentManager = new TopicContentManager(options.Project, settings.ExternalXmlDocumentationPath, settings.ProjectXmlFileName,
				mediaDirectoryProjectRelative, displayManager, importer);

			contentManager.ItemChanged += (s, e) => options.Project.IsSaved = false;

			propertiesManager = new TopicPropertiesManager(options.Project, settings.TopicsDirectory);
			propertiesManager.PropertyChanged += (s, e) => options.Project.IsSaved = false;
		}
		#endregion

		#region Methods
		#region Initialization
		public bool CancelReferenceTopicsBuild(bool wait)
		{
			bool canceled = false;

			if (buildReferenceTopicsAsyncResult != null)
			{
				canceled = true;
				referenceTopicsBuildContext.Cancel();

				if (wait && !buildReferenceTopicsAsyncResult.IsCompleted)
				{
					System.Diagnostics.Debug.WriteLine("Waiting to cancel reference build for " + projectName, "Topic Manager");

					canceled = buildReferenceTopicsAsyncResult.AsyncWaitHandle.WaitOne();

					System.Diagnostics.Debug.WriteLine("Reference build canceled for " + projectName, "Topic Manager");
				}
			}

			return canceled;
		}

		public void SuspendUpdateReferenceTopics()
		{
			System.Threading.Monitor.Enter(syncBuildReferenceTopics);
		}

		public void ResumeUpdateReferenceTopics()
		{
			System.Threading.Monitor.Exit(syncBuildReferenceTopics);
		}

		public void LoadTopics(bool forceRebuildReferenceToc)
		{
			System.Diagnostics.Debug.WriteLine("Loading conceptual topics for " + projectName, "Topic Manager");

			topics.Clear();

			LoadReferenceTopicsAsync(forceRebuildReferenceToc);
			LoadTopicsDocument();

			if (bufferEnabled && !conceptualTopicsBuffered)
				fillBufferDuringBind = true;

			dataManager.DataSource = DataSource;

			if (bufferEnabled)
			{
				conceptualTopicsBuffered = true;
				fillBufferDuringBind = false;
			}

			conceptualTopicsLoaded = true;

			System.Diagnostics.Debug.WriteLine("Done loading conceptual topics for " + projectName, "Topic Manager");
		}

		private void LoadTopicsDocument()
		{
			conceptualManager.Topics.Load(settings.BuildReferenceTopics);
		}

		private void LoadReferenceTopicsAsync(bool forceRebuildReferenceToc)
		{
			if (!settings.BuildReferenceTopics ||
				// topic management cannot generate a reference TOC during a project build
				(BuildController.ExecutingEngine != null && BuildController.ExecutingEngine.Project.Matches(options.Project)))
				return;
			else if (referenceTopicsBuildContext != null)
			// reference TOC generation is already occurring asynchronously
			{
				bool canceled = referenceTopicsBuildContext.Canceled;

				if (!canceled && !forceRebuildReferenceToc)
					// use the reference TOC being generated asynchronously
					return;
				else
				{
					if (!canceled)
						CancelReferenceTopicsBuild(true);

					// The EndLoadReferenceTopics callback invokes to the UI thread so it cannot run until this method returns.
					// Once executed, it will invoke the following continuation: 
					referenceBuildContinuation = new LoadReferenceTopicsInvoker(LoadReferenceTopicsAsync);
					return;
				}
			}

			BuildStepCollection steps = new BuildStepCollection(engine);
			referenceTopicsBuildContext = new BuildContext(engine, trace, steps);

			LoadReferenceTopicsInvoker task = new LoadReferenceTopicsInvoker(BeginLoadReferenceTopics);
			buildReferenceTopicsAsyncResult = task.BeginInvoke(forceRebuildReferenceToc, EndLoadReferenceTopics, new object[] { task, System.Threading.SynchronizationContext.Current });
		}

		private void BeginLoadReferenceTopics(bool forceRebuild)
		{
			trace.Clear();

			if (forceRebuild)
				apiFilter = null;

			bool rebuild = forceRebuild || rebuildRequired || engine.BuildType == BuildType.Full || engine.BuildType == BuildType.Partial;

			if (!referenceTopicsLoaded || rebuild || engine.HasSourcesChanged || !System.IO.File.Exists(tocFile) || !System.IO.File.Exists(reflectionFile))
			{
				if (referenceTopicsBuildContext.Canceled || !DocProjectEnvironment.IsInitialized)		// when uninitialized, the host is probably closing
					return;

				DocProjectEnvironment.Host.Status = "Building reference data for " + projectName;
				System.Diagnostics.Debug.WriteLine("Building reference data for " + projectName, "Topic Manager");

				tocNavigator = null;
				reflection = null;
				referenceTopicsLoaded = false;
				string ignore_me;

				// NOTE: trying to rebuild only the TOC may cause missing topics in the dialog (filtered topics) so it must be avoided.
				if (!engine.BuildToc(referenceTopicsBuildContext, false, true, false, false, out ignore_me))
				{
					if (!referenceTopicsBuildContext.Canceled)
						throw new InvalidOperationException(Errors.SandcastleTopicFilterBuildFailed);
				}
			}

			if (referenceTopicsBuildContext.Canceled || !DocProjectEnvironment.IsInitialized)		// when uninitialized, the host is probably closing
				return;

			if (tocNavigator == null)
			{
				XmlDocument document = new XmlDocument();
				document.Load(tocFile);

				tocNavigator = document.CreateNavigator();

				EnsureRootTopic(tocNavigator);
			}

			if (referenceTopicsBuildContext.Canceled || !DocProjectEnvironment.IsInitialized)		// when uninitialized, the host is probably closing
				return;

			if (reflection == null)
			{
				DocProjectEnvironment.Host.Status = "Loading reference topics for " + projectName;
				System.Diagnostics.Debug.WriteLine("Loading reference topics for " + projectName, "Topic Manager");

				reflection = new ReflectionDocument(options.DocumentationSetName, reflectionFile);
				reflection.Load();

				DocProjectEnvironment.Host.Status = "Done loading reference topics for " + projectName;
				System.Diagnostics.Debug.WriteLine("Done loading reference topics for " + projectName, "Topic Manager");
			}

			if (referenceTopicsBuildContext.Canceled || !DocProjectEnvironment.IsInitialized)		// when uninitialized, the host is probably closing
				return;

			if (apiFilter == null)
			{
				configDocument.Load(mRefBuilderConfigFile);
				apiFilter = configDocument.ApiFilter;
			}
		}

		private static void EnsureRootTopic(IXPathNavigable toc)
		{
			XPathNavigator navigator = toc.CreateNavigator();

			if (!navigator.MoveToFollowing(XPathNodeType.Element))
			{
				navigator.AppendChildElement(null, "topics", null, null);
				navigator.MoveToFollowing(XPathNodeType.Element);
			}

			if (!navigator.MoveToFollowing(XPathNodeType.Element))
				InsertRootTopic(navigator);
			else
			{
				// move to root element
				navigator.MoveToParent();

				XPathNavigator root = InsertRootTopic(navigator);
				navigator = root.Clone();

				while (navigator.MoveToNext(XPathNodeType.Element) && !navigator.IsSamePosition(root))
				{
					root.AppendChild(navigator);
					navigator.DeleteSelf();
					navigator = root.Clone();
				}
			}
		}

		private static XPathNavigator InsertRootTopic(IXPathNavigable toc)
		{
			XPathNavigator navigator = toc.CreateNavigator();

			navigator.PrependChildElement(null, "topic", null, null);
			navigator.MoveToChild(XPathNodeType.Element);
			navigator.CreateAttribute(null, "id", null, SandcastleRootReferenceProjectId);
			navigator.CreateAttribute(null, "project", null, string.Empty);
			navigator.CreateAttribute(null, "file", null, string.Empty);

			return navigator;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void EndLoadReferenceTopics(IAsyncResult result)
		{
			System.Diagnostics.Debug.WriteLineIf(referenceTopicsBuildContext.Canceled && DocProjectEnvironment.IsInitialized,
				"Canceled build of reference topics for: " + projectName, "Topic Manager");

			object[] state = (object[]) result.AsyncState;
			LoadReferenceTopicsInvoker task = (LoadReferenceTopicsInvoker) state[0];
			System.Threading.SynchronizationContext sync = (System.Threading.SynchronizationContext) state[1];

			// NOTE: Locking first prevents a bug in the DocProject external UI that seems to be caused when a modal dialog remains open
			// while this code is executing asynchronously.
			lock (syncBuildReferenceTopics)
			{
				sync.Send(delegate(object innerState)
				{
					try
					{
						task.EndInvoke(result);

						if (referenceTopicsBuildContext.Canceled || !DocProjectEnvironment.IsInitialized)		// when uninitialized, the host is probably closing
							return;

						XPathNavigator referenceTopicsRoot = tocNavigator.SelectSingleNode("/topics");
						referenceTopicsRoot.MoveToFirstChild();

						TopicsDocument.MakeApiRoot(referenceTopicsRoot);

						if (bufferEnabled && !referenceTopicsBuffered)
							fillBufferDuringBind = true;

						try
						{
							dataManager.ReplaceElement(conceptualManager.Topics.ApiRoot, referenceTopicsRoot);
						}
						catch (InvalidOperationException)
						// the tree is no longer bound to a data source
						{
							System.Diagnostics.Debug.WriteLine("Tree not bound to data source for project: " + projectName, "Topic Manager");
							return;
						}

						if (bufferEnabled)
						{
							referenceTopicsBuffered = true;
							fillBufferDuringBind = false;
						}

						referenceTopicsLoaded = true;
						rebuildRequired = false;
					}
					catch (Exception ex)
					{
						System.Diagnostics.Debug.WriteLine("Error while building or loading reference topics for " + projectName + ": " + ex.Message);

						OnAsyncError(new AsyncCompletedEventArgs(ex, false, this));
					}
					finally
					{
						buildReferenceTopicsAsyncResult = null;
						referenceTopicsBuildContext = null;

						if (referenceBuildContinuation != null)
						{
							referenceBuildContinuation(false);
							referenceBuildContinuation = null;
						}
						else
						{
							IEnvironmentHost host = DocProjectEnvironment.Host;

							if (host != null)		// occurred in testing when VS was shutting down
								host.Status = "Ready";
						}
					}
				}, null);
			}
		}

		public TopicNode CreateNode(IXPathNavigable topicElement)
		{
			if (topicElement == null)
				throw new ArgumentNullException("topicElement");

			XPathNavigator element = topicElement.CreateNavigator();

			if (string.Equals(element.Name, TopicsDocument.ApiRootPlaceholderElementName, StringComparison.Ordinal))
			{
				apiPlaceholderTopic = new Topic(TopicType.Project, element, "Loading", Resources.Text.LoadingReferenceTopics);

				topics[element] = apiPlaceholderTopic;

				// A clone is required to move to the parent since the element reference is already being used as a key in the topics dictionary.
				XPathNavigator clone = element.Clone();

				if (clone.MoveToParent() && topics.ContainsKey(clone))
					apiPlaceholderTopic.Parent = topics[clone];

				return new TopicNode(apiPlaceholderTopic, "Clock");
			}
			else
			{
				if (TopicsDocument.IsApiRoot(element))
				{
					topics.Remove(apiPlaceholderTopic.Element);

					TopicNode node = CreateReferenceNode(element);
					apiPlaceholderTopic = node.Topic;
					return node;
				}
				else if (TopicsDocument.IsApiDescendant(element))
					return CreateReferenceNode(element);
				else
					return CreateConceptualNode(element);
			}
		}

		private TopicNode CreateConceptualNode(XPathNavigator element)
		{
			string id = element.GetAttribute("id", "");
			string file = element.GetAttribute("file", "");

			if (string.IsNullOrEmpty(id))
				return null;

			bool isContainer = string.IsNullOrEmpty(file);
			bool isHtml = !isContainer &&
				(file.EndsWith(".htm", StringComparison.OrdinalIgnoreCase)
				|| file.EndsWith(".html", StringComparison.OrdinalIgnoreCase));

			Topic topic;

			if (isContainer || isHtml)
			{
				// NOTE: Topic titles cannot be changed in reference documentation so the following code must not appear in the CreateReferenceNode method.
				string title = null;

				if (boundTopicData.ContainsKey(element))
					title = boundTopicData[element].TocTitle;

				if (title == null && cachedTopicData.ContainsKey(element))
					title = cachedTopicData[element].TocTitle;

				if (title == null)
					title = element.GetAttribute("title", "");

				topic = new Topic((isHtml) ? TopicType.Html : TopicType.Container, element, id, title, file);
			}
			else
			{
				topic = new Topic(TopicType.Conceptual, element, id, null, file);
				topic.Metadata = propertiesManager[topic];
			}

			// NOTE: The element will already exist in the collection when a node is moved (drag & drop)
			topics[element] = topic;

			// A clone is required to move to the parent since the element reference is already being used as a key in the topics dictionary.
			XPathNavigator clone = element.Clone();

			if (clone.MoveToParent() && topics.ContainsKey(clone))
				topic.Parent = topics[clone];

			TopicNode node = new TopicNode(topic);

			SetCheckedState(element, node);

			if (fillBufferDuringBind)
				CacheTopicData(topic);

			return node;
		}

		private TopicNode CreateReferenceNode(XPathNavigator element)
		{
			string id = element.GetAttribute("id", "");
			string file = element.GetAttribute("file", "");

			if (string.IsNullOrEmpty(id))
				id = SandcastleRootReferenceProjectId;

			if (!string.IsNullOrEmpty(file))
				file = System.IO.Path.GetFileNameWithoutExtension(file);

			ReflectionApiElement reflectionElement = reflection.TryGetElement(file ?? id);

			Topic topic;

			if (topics.ContainsKey(element))
				// NOTE: When re-binding without reloading, the topics collection will be filled already so individual topics should 
				// be retrieved instead of being re-created.  Re-binding will occur when the Topic Management dialog is used as well
				// as Topic Explorer because they both reference the same TopicManager instance but use different tree control instances.
				// (The only thing that changes between the clone and the original topic is the XML element reference.  This supports
				// drag & drop operations in the tree since it causes elements to be recreated.)
				topic = new Topic(topics[element], element);
			else
				topic = null;

			if (reflectionElement == null)
			{
				if (topic == null)
				{
					topic = new Topic(TopicType.Project, element, id, ConceptualTopicFileManager.ApiProjectTopicText);

					topics.Add(element, topic);
				}
			}
			else
			{
				if (topic == null)
				{
					topic = new Topic(reflectionElement, element, id);

					topics.Add(element, topic);
				}
			}

			TopicNode node = new TopicNode(topic);

			// A clone is required to move to the parent since the element reference is already being used as a key in the topics dictionary.
			XPathNavigator clone = element.Clone();

			if (clone.MoveToParent())
			{
				if (topic.IsType && reflectionElement != null && !string.IsNullOrEmpty(reflectionElement.ParentTypeId))
					// Sandcastle displays nested types on the same level in the TOC as their parent types.  So to assign the correct Parent to the 
					// topic it must be located first by its ID.
					clone = clone.SelectSingleNode("//*[@id='" + reflectionElement.ParentTypeId + "']");

				if (topics.ContainsKey(clone))
					topic.Parent = topics[clone];
			}

			SetCheckedState(element, node);

			if (fillBufferDuringBind)
				CacheTopicData(topic);

			return node;
		}

		private void SetCheckedState(IXPathNavigable element, TopicNode topicNode)
		{
			bool? exposed = null;

			if (boundTopicData.ContainsKey(element))
				exposed = boundTopicData[element].Expose;

			if (exposed == null && cachedTopicData.ContainsKey(element))
				exposed = cachedTopicData[element].Expose;

			if (exposed == null)
				exposed = IsExposed(topicNode);

			if (!exposed.Value)
				topicNode.Checked = false;
			else
			{
				topicNode.Checked = true;
				topicNode.Topic.Expose = true;

				TopicNode parentNode = topicNode.Parent;

				while (parentNode != null)
				{
					Topic parent = parentNode.Topic;

					if (!parentNode.Checked)
					{
						parentNode.Checked = true;
						parent.Expose = true;

						if (fillBufferDuringBind)
						{
							string title = (cachedTopicData.ContainsKey(parent.Element))
								? cachedTopicData[parent.Element].TocTitle
								: null;

							cachedTopicData[parent.Element] = new TopicData(true, title);
						}
					}

					parentNode = parentNode.Parent;
				}
			}
		}

		private bool IsExposed(TopicNode node)
		{
			Topic topic = node.Topic;

			switch (topic.TopicType)
			{
				case TopicType.Html:
				case TopicType.Container:
				case TopicType.Conceptual:
					if (node.Parent != null && !((TopicNode) node.Parent).Checked)
						return false;
					else
						return topic.Expose;
				case TopicType.Project:
					return apiFilter.ApiExposed;
				case TopicType.Namespace:
					return apiFilter.IsNamespaceExposed(topic.Name);
				case TopicType.Module:
					return apiFilter.IsTypeExposed(topic.ParentNamespace.Name, topic.Name);
				default:
					if (topic.IsType)
						return apiFilter.IsTypeExposed(topic.ParentNamespace.Name, topic.Name);
					else
						return apiFilter.IsMemberExposed(topic.ParentNamespace.Name, topic.ParentType.Name, topic.Name);
			}
		}

		private void CacheTopicData(Topic topic)
		{
			cachedTopicData[topic.Element] = new TopicData(topic.Expose,
				(topic.TopicType == TopicType.Container || topic.TopicType == TopicType.Html) ? topic.TocTitle : null);
		}

		private void UpdateBoundTopicData(IXPathNavigable element, bool updateExpose, bool updateTitle)
		{
			if (topics.ContainsKey(element))
			{
				Topic topic = topics[element];
				TopicData data;

				if (boundTopicData.ContainsKey(element))
				{
					data = boundTopicData[element];
					data = new TopicData(
						(updateExpose) ? topic.Expose : data.Expose,
						(updateTitle)
							? (topic.TopicType == TopicType.Container || topic.TopicType == TopicType.Html) ? topic.TocTitle : null
							: data.TocTitle);
				}
				else
				{
					data = new TopicData(
						(updateExpose) ? (bool?) topic.Expose : null,
						(updateTitle)
							? (topic.TopicType == TopicType.Container || topic.TopicType == TopicType.Html) ? topic.TocTitle : null
							: null);
				}

				boundTopicData[element] = data;
			}
		}
		#endregion

		#region MRefBuilder config and filter management
		private bool ShouldExpose(Topic topic)
		{
			int totalCount, exposedCount;
			ShouldExposeRecursive(topic.Element.CreateNavigator(), out totalCount, out exposedCount);

			return (totalCount == 0)
				? topic.Expose
				: exposedCount >= (int) Math.Ceiling(totalCount / 2D);
		}

		private void ShouldExposeRecursive(XPathNavigator element, out int totalCount, out int exposedCount)
		{
			totalCount = 0;
			exposedCount = 0;

			foreach (XPathNavigator subTopic in element.SelectChildren(XPathNodeType.Element))
			{
				Topic topic = topics[subTopic];

				if (topic.TopicType == TopicType.General)
				{
					int total, exposed;
					ShouldExposeRecursive(subTopic, out total, out exposed);

					totalCount += total;
					exposedCount += exposed;
				}
				else
				{
					totalCount++;

					if (topic.Expose)
						exposedCount++;
				}
			}
		}

		private void SaveApiFilter()
		{
			if (apiPlaceholderTopic == null)
				return;

			apiFilter.Clear();

			apiFilter.ApiExposed = ShouldExpose(apiPlaceholderTopic);

			// Apply each topic in order but don't add a topic that will inherit visiblitiy from its parent.  If a topic
			// is required but its parent was not already added then the parent will be added at that time with the 
			// appropriate value for its expose attribute.  (lazily)

			SaveNamespaceFilters();

			SaveTypeFilters();

			SaveMemberFilters();

			options.Project.GetItem(configDocument.ConfigurationFile).Checkout();

			configDocument.Save();
		}

		private void SaveNamespaceFilters()
		{
			bool exposeApi = apiFilter.ApiExposed;

			foreach (Topic topic in topics.Values)
			{
				if (topic.TopicType == TopicType.Namespace)
				{
					bool expose = topic.ShouldExpose = ShouldExpose(topic);

					if (expose != exposeApi)
						apiFilter.AppendNamespaceNode(topic.Name, expose);
				}
			}
		}

		private void SaveTypeFilters()
		{
			foreach (Topic topic in topics.Values)
			{
				if (topic.IsType || topic.TopicType == TopicType.Module)
				{
					Topic parentNamespace = topic.ParentNamespace;

					bool expose = topic.ShouldExpose = ShouldExpose(topic);

					if (expose != parentNamespace.ShouldExpose)
						AddTypeFilter(parentNamespace, topic, expose);
				}
			}
		}

		private void SaveMemberFilters()
		{
			foreach (Topic topic in topics.Values)
			{
				if (topic.IsMember && !topic.HasMethodOverloads)
				// If topic represents a method then individual overloads are not added.  Only the overloads page itself is
				// added unless there is no overloads topic, in which case the method is added.  The reason is because 
				// the apiFilter used by the MRefBuilder program does not accept method signatures so there is no way of
				// identifying a single overload - it's all or nothing.
				{
					Topic parentType = topic.ParentType;
					Topic parentNamespace = parentType.ParentNamespace;

					if (apiFilter.HasMember(parentNamespace.Name, parentType.Name, topic.Name))
						// possibly an overloaded method that has already been added to the filter
						continue;

					bool expose = topic.ShouldExpose = ShouldExpose(topic);

					if (expose != parentType.ShouldExpose)
						AddMemberFilter(parentNamespace, parentType, topic, expose);
				}
			}
		}

		private TypeConfigNode AddTypeFilter(Topic @namespace, Topic type, bool expose)
		{
			NamespaceConfigNode namespaceFilter = apiFilter[@namespace.Name];

			if (namespaceFilter == null)
				// namespace inherits visibility from the apiFilter but since it's needed
				// for the current type it must be added now
				namespaceFilter = apiFilter.AppendNamespaceNode(@namespace.Name, @namespace.ShouldExpose);

			Topic parent = type.ParentType;
			string name;

			if (parent != null)
				// qualify nested types
				name = parent.Name + "." + type.Name;
			else
				name = type.Name;

			return TypeConfigNode.AppendTo(namespaceFilter, name, expose);
		}

		private MemberConfigNode AddMemberFilter(Topic @namespace, Topic type, Topic member, bool expose)
		{
			TypeConfigNode typeFilter = apiFilter[@namespace.Name, type.Name];

			if (typeFilter == null)
				// type inherits visibility from its namespace but since it's needed
				// for the current member it must be added now
				typeFilter = AddTypeFilter(@namespace, type, type.ShouldExpose);
			
			return MemberConfigNode.AppendTo(typeFilter, member.Name, expose);
		}
		#endregion

		#region Conceptual Topics
		public void OpenConceptualTopic(IXPathNavigable element)
		{
			OpenConceptualTopic(topics[element]);
		}

		public void OpenConceptualTopic(Topic topic)
		{
			if (topic == null)
				throw new ArgumentNullException("topic");
			else if (string.IsNullOrEmpty(topic.File))
				return;

			string path = System.IO.Path.Combine(conceptualManager.TopicsPath, topic.File);

		tryAgain:
			try
			{
				IProjectItem item = options.Project.GetItem(path);
				item.Show();	// NOTE: This is required otherwise an exception may be thrown by the Open method if it's not a project item.

				if (DocProjectEnvironment.RunningInVisualStudio)
				{
					item.Open();
				}
				else
				// IProjectItem.Open() is not used in this case because the MSBuildProjectItem implementation will call Process.Start.
				// The default program for XML is usually the browser, but the file should be editable instead...
				{
					VisualStudioInstance vs = VSEnvironmentHost.DefaultInstance;		// NOTE: the DTE is not used if it's not initialized (i.e., in Express editions)

					if (vs == null)
						System.Diagnostics.Process.Start("notepad.exe", "\"" + item.Path + "\"");
					else
					{
						System.Diagnostics.Process.Start(vs.ProgramPath, "/edit \"" + item.Path + "\"");
						/*
						 * Unfortunately, it doesn't appear to be possible to load the authoring schemas into VS as well.  I tried: 
						 * 
						 * - devenv /edit file /edit schema_path_1 /edit schema_path_2			: doesn't work
						 * - devenv /edit file "open schema_path_1 schema_path_2"						: this works if the schema paths don't have spaces, but they do (Program Files).  And there is no possible way to include nested quotes that work.
						 * - Various other combinations of qutoes and paths, such as using environment variables to eliminate spaces or specifying a folder name instead of individual files.
						 * - Calling Process.Start in a loop																: wow, what a mess; even with WaitForInputIdle and a fair timeout VS would still pop up failure messages and open new instances of VS, simultaneously.
						 * 
						 * The purpose was for Express uesrs, who unfortnuately will have to load the schemas into VS manually if they want IntelliSense.  Boo!
						 * I don't feel compelled to work on a solution using automation for VS Standard+ though.
						 */
					}
				}
			}
			catch (ArgumentException) { }
			catch (System.IO.FileNotFoundException ex)
			{
				if (path.EndsWith(".htm", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".html", StringComparison.OrdinalIgnoreCase))
				{
					DocProjectEnvironment.ShowErrorDialogWithCaption(Resources.Text.InvalidItemFilePathCaption,
						Resources.Errors.FileNotFound, ex.FileName);
				}
				else if (DocProjectEnvironment.ShowErrorDialogWithCaption(MessageBoxButtons.YesNo,
					Resources.Text.InvalidItemFilePathCaption, Resources.Errors.InvalidItemFilePath,
					ex.FileName, Environment.NewLine) == DialogResult.Yes)
				{
					conceptualManager.CreateTopicFile(topic, true);

					goto tryAgain;
				}
			}
		}

		public IXPathNavigable InsertNewTopic(IXPathNavigable parent, ConceptualTemplate template, Guid id,
			TopicType type, string topicName, string metadataFile)
		{
			if ((type == TopicType.Conceptual || type == TopicType.Html) && template == null)
				throw new ArgumentNullException("template", Resources.Errors.ConceptualTemplateRequired);

			IXPathNavigable element = dataManager.InsertNewChild(parent, "topic");

			if (topicName == null || (topicName = topicName.Trim()).Length == 0)
			{
				if (type == TopicType.Conceptual)
					topicName = template.Name;
				else if (type == TopicType.Html)
					topicName = System.IO.Path.GetFileName(template.FullPath);
				else
					topicName = Resources.Text.Container;
			}

			Topic topic;

			switch (type)
			{
				case TopicType.Conceptual:
					topic = conceptualManager.CreateNewTopic(element, id, topicName, template, metadataFile,
						checkId =>
						{
							foreach (Topic t in topics.Values)
								if (t.Id == checkId.ToString())
									return false;

							return true;
						});

					topic.Metadata = propertiesManager[topic];
					break;
				case TopicType.Container:
					topic = new Topic(TopicType.Container, element, ((id == Guid.Empty) ? Guid.NewGuid() : id).ToString(), topicName);
					break;
				case TopicType.Html:
					SystemPath file = new SystemPath(template.FullPath, false, false);
					SystemPath target = new SystemPath(settings.TopicsHtmlDirectory, topicName, false, false);

					file.CopyTo(target, true, true);

					topic = new Topic(TopicType.Html, element, topicName, System.IO.Path.GetFileNameWithoutExtension(topicName), 
						target.RelativeTo(settings.TopicsDirectory));

					break;
				default:
					throw new ArgumentException(Resources.Errors.InvalidTopicType, "type");
			}

			if (parent != null && !parent.CreateNavigator().IsSamePosition(conceptualManager.Topics.Root.CreateNavigator()))
			{
				Topic parentTopic = topics[parent];

				topic.Parent = parentTopic;
				topic.Expose = parentTopic.Expose;
			}

			topics.Add(element, topic);

			return element;
		}
		#endregion

		#region State Persistence
		public void BeginBufferChanges()
		{
			bufferEnabled = true;
			referenceTopicsBuffered = false;
			conceptualTopicsBuffered = false;

			if (dynamicFilters == null)
				originalDynamicFilters = null;
			else
				originalDynamicFilters = new TopicFilterCollection(dynamicFilters);
		}

		public void EndBufferChanges(bool commit)
		{
			bufferEnabled = false;

			if (commit)
				CommitChanges();
			else
				CancelChanges();

			cachedTopicData.Clear();
		}

		private void CommitChanges()
		{
			// Data is committed but isn't actually saved until Save() is called.
			contentManager.CommitChanges();
			propertiesManager.CommitChanges();
		}

		private void CancelChanges()
		{
			// NOTE: Do not set dirty state flags to false here since only the last buffered changes must be caneled.
			// Previously committed changes that have not yet been saved to disc must not be canceled.

			if (dynamicFilters != null)
			{
				dynamicFilters.SuspendChangeTacking();

				try
				{
					dynamicFilters.Clear();

					if (originalDynamicFilters != null)
						// NOTE: Repopulate the list instead of reassigning the field's reference so that existing event handlers are kept.
						dynamicFilters.AddRange(new TopicFilterCollection(originalDynamicFilters));		// clone
				}
				finally
				{
					dynamicFilters.ResumeChangeTracking(true);
					dynamicFilters.HasChanges = false;
				}

				if (originalDynamicFilters != null)
					originalDynamicFilters.Clear();
			}

			contentManager.CancelChanges();
			propertiesManager.CancelChanges();

			foreach (Topic topic in topics.Values)
			{
				if (cachedTopicData.ContainsKey(topic.Element))
				{
					TopicData data = cachedTopicData[topic.Element];

					if (data.Expose != null)
						topic.Expose = data.Expose.Value;

					if (data.TocTitle != null)
						topic.TocTitle = data.TocTitle;

					boundTopicData[topic.Element] = data;
				}
			}
		}

		public void SaveChanges()
		{
			SaveChangesInternal(TopicManagerChanges.None);
		}

		public void SaveChanges(TopicManagerChanges changes)
		{
			SaveChangesInternal(changes);
		}

		private void SaveChangesInternal(TopicManagerChanges changesSubset)
		{
			TopicManagerChanges changes = (changesSubset == TopicManagerChanges.None) ? Changes : Changes & changesSubset;

			OnSaving(new TopicManagerChangesEventArgs(changes));

			// the OnSaving event gives handlers a chance to commit buffered changes, so the Changes property must be read again
			changes = (changesSubset == TopicManagerChanges.None) ? Changes : Changes & changesSubset;

			System.Diagnostics.Debug.WriteLine("Saving: " + changes.ToString(), "TopicManager");

			SaveMRefBuilderConfig((changes & TopicManagerChanges.Filters) == TopicManagerChanges.Filters);

			if ((changes & TopicManagerChanges.DynamicFilters) == TopicManagerChanges.DynamicFilters)
			{
				IProjectItem file = options.Project.GetItem(dynamicFiltersFile);

				file.Checkout();

				TopicFilter.SaveFilters(dynamicFilters, dynamicFiltersFile);

				dynamicFilters.HasChanges = false;

				file.Show();
			}

			if ((changes & TopicManagerChanges.Toc) == TopicManagerChanges.Toc)
			{
				IProjectItem file = options.Project.GetItem(conceptualManager.Topics.FilePath);

				file.Checkout();

				conceptualManager.Topics.Save();

				file.Show();

				topicsDirty = false;
			}

			if ((changes & TopicManagerChanges.Documentation) == TopicManagerChanges.Documentation)
			{
				if (!options.BufferChanges)
					contentManager.CommitChanges();

				contentManager.SaveChanges(topics.Values);
			}

			if ((changes & TopicManagerChanges.Properties) == TopicManagerChanges.Properties)
			{
				if (!options.BufferChanges)
					propertiesManager.CommitChanges();
				
				propertiesManager.SaveChanges();
			}

			boundTopicData.Clear();

			System.Diagnostics.Debug.WriteLine("Saved: " + changes.ToString(), "TopicManager");

			OnSaved(new TopicManagerChangesEventArgs(changes));
		}

		private void SaveMRefBuilderConfig(bool saveApiFilter)
		{
			if (saveApiFilter)
			{
				SaveApiFilter();
				filterDirty = false;
			}
		}
		#endregion

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public XmlContentDocument GetContentDocument(Topic topic)
		{
			return contentManager.GetContentDocument(topic);
		}

		public TopicMetadata GetMetadata(Topic topic)
		{
			return propertiesManager[topic];
		}

		public TopicMetadata GetMetadata(IXPathNavigable element)
		{
			return propertiesManager[topics[element]];
		}

		public override string ToString()
		{
			if (topics.Count == 0)
				return string.Empty;

			int topicsCount = topics.Count;

			if (!referenceTopicsLoaded && settings.BuildReferenceTopics)
				// don't count the API root topic placeholder
				topicsCount--;

			return topicsCount + " topic(s)";
		}
		#endregion

		#region Events
		private readonly object AsyncErrorEventLock = new object();
		private EventHandler<AsyncCompletedEventArgs> AsyncErrorEvent;
		private readonly object SavedEventLock = new object();
		private EventHandler<TopicManagerChangesEventArgs> SavedEvent;
		private readonly object SavingEventLock = new object();
		private EventHandler<TopicManagerChangesEventArgs> SavingEvent;

		/// <summary>
		/// Event raised when the <see cref="SaveChanges"/> method is called.
		/// </summary>
		public event EventHandler<TopicManagerChangesEventArgs> Saving
		{
			add
			{
				lock (SavingEventLock)
				{
					SavingEvent += value;
				}
			}
			remove
			{
				lock (SavingEventLock)
				{
					SavingEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="Saving" /> event.
		/// </summary>
		/// <param name="e"><see cref="TopicManagerChangesEventArgs" /> object that provides the arguments for the event.</param>
		private void OnSaving(TopicManagerChangesEventArgs e)
		{
			EventHandler<TopicManagerChangesEventArgs> handler = null;

			lock (SavingEventLock)
			{
				handler = SavingEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised after the <see cref="SaveChanges"/> method is called.
		/// </summary>
		public event EventHandler<TopicManagerChangesEventArgs> Saved
		{
			add
			{
				lock (SavedEventLock)
				{
					SavedEvent += value;
				}
			}
			remove
			{
				lock (SavedEventLock)
				{
					SavedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="Saved" /> event.
		/// </summary>
		/// <param name="e"><see cref="TopicManagerChangesEventArgs" /> object that provides the arguments for the event.</param>
		private void OnSaved(TopicManagerChangesEventArgs e)
		{
			EventHandler<TopicManagerChangesEventArgs> handler = null;

			lock (SavedEventLock)
			{
				handler = SavedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when an exception is thrown during an asynchronous operation.
		/// </summary>
		public event EventHandler<AsyncCompletedEventArgs> AsyncError
		{
			add
			{
				lock (AsyncErrorEventLock)
				{
					AsyncErrorEvent += value;
				}
			}
			remove
			{
				lock (AsyncErrorEventLock)
				{
					AsyncErrorEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="AsyncError" /> event.
		/// </summary>
		/// <param name="e"><see cref="AsyncTaskCompletedCallback" /> object that provides the arguments for the event.</param>
		private void OnAsyncError(AsyncCompletedEventArgs e)
		{
			EventHandler<AsyncCompletedEventArgs> handler = null;

			lock (AsyncErrorEventLock)
			{
				handler = AsyncErrorEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region Event Handlers
		private void DocProjectEnvironment_ProjectBuildComplete(object sender, DocProjectEventArgs e)
		{
			if (buildReferenceTopicsAsyncResult == null && e.Project.Matches(options.Project))
				// A rebuild is required since one or more filters may have been applied during a normal build
				rebuildRequired = true;
		}

		private void dataManager_ElementChanged(object sender, ElementChangedEventArgs e)
		{
			switch (e.Change)
			{
				case ElementChangeType.CheckChanged:
					if (TopicsDocument.IsApiDescendant(e.Element))
						filterDirty = true;
					else
						topicsDirty = true;

					UpdateBoundTopicData(e.Element, true, false);

					options.Project.IsSaved = false;
					break;
				case ElementChangeType.Deleted:
					topicsDirty = true;
					options.Project.IsSaved = false;

					conceptualManager.ForEachElementRecursive(e.Element, delegate(IXPathNavigable element)
					{
						topics.Remove(element);
					});
					break;
				case ElementChangeType.Created:
				case ElementChangeType.Edited:
					UpdateBoundTopicData(e.Element, false, true);

					topicsDirty = true;
					options.Project.IsSaved = false;
					break;
				case ElementChangeType.Moved:
					topicsDirty = true;
					options.Project.IsSaved = false;
					goto case ElementChangeType.Replaced;
				case ElementChangeType.Replaced:
					/*IXPathNavigable original = e.OriginalElement;

					bool apiRootChanged = original != null && TopicsDocument.IsApiRoot(original) || TopicsDocument.ContainsApiRoot(original);*/

					topics.Remove(e.Element);
					break;
			}
		}

		private void dynamicFilters_ListChanged(object sender, ListChangedEventArgs e)
		{
			options.Project.IsSaved = false;
		}
		#endregion
	}
}
